Skip to content

Experimental OPF Implementation using MATPOWER Input - #594

Open
lukelowry wants to merge 16 commits into
developfrom
lukel/OPF-NEW-dev
Open

lukelowry wants to merge 16 commits into
developfrom
lukel/OPF-NEW-dev

Conversation

@lukelowry

@lukelowry lukelowry commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Experimental @superwhiskers @nkoukpaizan which uses MATPOWER input which makes interfacing with PhasorDynamics harder but most OPF data comes in MATPOWER, so this is probably most practical choice

  • Application OptimalDispatch

Checklist

  • All tests pass.
  • Code compiles cleanly with flags -Wall -Wpedantic -Wconversion -Wextra.
  • The new code follows GridKit™ style guidelines.
  • There are unit tests for the new code.
  • The new code is documented.
  • The feature branch is rebased with respect to the target branch.
  • The CHANGELOG.md has been updated to reflect the changes. If this is a minor PR that is part of a larger fix already included in the file, state so.

Further comments

Needs some cleaning first

@pelesh

pelesh commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator

A few comments:

  • If you have JSON input parser already available, I suggest you use that instead of MATPOWER format. I was thinking you could use some of the existing power flow implementation, but since you took a different route, I suggest you ditch MATPOWER and go straight to JSON.
  • I would have optimal power flow app in EconomicDispatch subdirectory in examples directory, at the same level as PhasorDynamics.
  • I would rename Optimization/OptimizationProblem.*pp files (and the class within) into Optimization/SteadyState.*pp. We will likely have Multiperiod and MixedInteger as new optimization modules in the future.

@pelesh
pelesh requested a review from abirchfield September 28, 2026 18:22
@lukelowry

lukelowry commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator Author

@pelesh I changed it so it uses the PowerFlow parser with very few changes. I do believe its valuable to have that as the input format only because it makes validation easier and there is practically unlimited data in .m, and OPF data does not always come with dynamic case files.

I believed I addressed your other issues too. I am able to use the example app to change the objective/model, solve for new operating point, and start simulation in steady state at that new point!

@lukelowry
lukelowry force-pushed the lukel/OPF-NEW-dev branch 4 times, most recently from 95913b8 to 5c4fb92 Compare October 10, 2026 21:02

@pelesh pelesh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very nice work. I suggest limiting the scope of this PR to providing ACOPF module only. Imho, the integration with dynamic simulations modules should be done after ACOPF is tested and input file format specified.

@superwhiskers superwhiskers left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tests pass on my end. i think it works quite well, but there are a few implementation questions we should sort out before this moves forward.

additionally, if we are to continue with matpower, are we intending to handle capability curves for generators or the convention of a dispatchable load as a generator with negative real-power injection? if they are outside the intended scope, could we reject them and document that limitation?

i think these conventions, particularly the latter, strengthen the argument for going with our own format, as using an existing format such as matpower requires us to adhere to its conventions, which may not be desirable.

Comment on lines +21 to +22
/// Relative tolerance of the optimal cost against a reference cost
constexpr double COST_TOLERANCE = 1.0e-8;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there not a situation in which one may want to configure this?

@pelesh pelesh Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the user will want to specify the tolerance. And this tolerance is too tight.

Comment on lines +17 to +34
/**
* @brief Coordinate entries in global indices
*
* Filled by `mapped_store` and `lower_store`. Assembly sums duplicate
* coordinates.
*/
struct CooEntries
{
std::vector<size_t> rows;
std::vector<size_t> cols;
std::vector<double> values;

void clear()
{
rows.clear();
cols.clear();
values.clear();
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should parameterize this by index type / scalar type so that you don't need the static assertion that IdxT/ScalarT are size_t/double in the evaluators

}

private:
__attribute__((always_inline)) inline static ScalarT lagrangian(const ModelT* model,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefer [[gnu::always_inline]]

Comment on lines +30 to +31
* @tparam ModelT - model type with `VARIABLE_SIZE` local variables and
* `CONSTRAINT_SIZE` local constraints

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VARIABLE_SIZE may be more accurately named VARIABLE_COUNT, NUM_VARIABLES, VARIABLE_ARRAY_SIZE, or something else. VARIABLE_SIZE to me read as "bit/byte width of individual variables"

Comment on lines +30 to +31
* @tparam ModelT - model type with `VARIABLE_SIZE` local variables and
* `CONSTRAINT_SIZE` local constraints

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

following what i did here, i'd recommend establishing these requirements on the ModelT parameter as a concept to reduce the amount of error noise for the end user

* @param[in] col_indices - Global column of each local column
* @param[in,out] entries - Entries in global indices
*/
[[maybe_unused]] __attribute__((always_inline)) static void mapped_store(double val,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
[[maybe_unused]] __attribute__((always_inline)) static void mapped_store(double val,
[[maybe_unused, gnu::always_inline]] static void mapped_store(double val,

static constexpr IdxT INTERNAL_VARIABLE_SIZE = 0;
/// Internal constraint functions: squared apparent power at bus 1 and bus 2
static constexpr IdxT INTERNAL_CONSTRAINT_SIZE = 2;
static constexpr bool CONSTANT = false;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would like you to document the meaning of this parameter somewhere. one place to do that would be on the concept that i proposed in another review comment for compile-time validation of optimal power flow components

/**
* @brief Line or off-nominal transformer between two buses
*
* Uses the admittance of `PhasorDynamics::Branch`. Power is positive

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"uses the admittance of PhasorDynamics::Branch" and similar comments referencing how the modeling code is based off that in the phasor dynamics modeling library are not worth retaining as they could diverge in the future, imo

const auto entry = actual.find(coordinate);
if (entry == actual.end())
{
std::cerr << "Entry (" << coordinate.first << ", " << coordinate.second << ") is missing\n";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should be using the logger here and elsewhere in these tests


auto& generator = data.generator.emplace_back();
generator.id = std::format("gen_{}", i + 1);
generator.in_service = row.status > 0;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because SystemModelData parameterizes the power flow data types with size_t, -1 (which is a valid status value) will be converted to a positive value, which will misinterpret the model.

possible solutions:

  • don't use matpower, use our own json modeling capabilities which could represent it as a boolean
  • change how status is represented, don't tie it to IdxT, probably parse it to a boolean but read it as int or something

@nkoukpaizan nkoukpaizan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall nice work! The component implementations are clear and I like the consolidated ComponentModelImpl.hpp.

A few comments:

  • For Enzyme, can we re-use some of the existing boilerplate for LowerSparseStorage and use buffers instead of CooEntries wrapping std::vector)? Feel free to amend the wrappers.
  • Can we bind GridKit::LinearAlgebra::Vector at the component level instead of std::vector? This could be a follow-up PR.
  • Several methods, e.g., in OptimizationEvaluator* and SteadyState* don't have a Doxygen documentation.

Comment on lines +402 to +404
static constexpr RealT POWER_TOL = 2 * std::numeric_limits<RealT>::epsilon();
static constexpr RealT JACOBIAN_TOL = 2 * std::numeric_limits<RealT>::epsilon();
static constexpr RealT HESSIAN_TOL = 9 * std::numeric_limits<RealT>::epsilon();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would round to 10 * and use the same tolerance.

Comment thread tests/UnitTests/CMakeLists.txt Outdated
Comment on lines +6 to +8
if(GRIDKIT_ENABLE_ENZYME)
add_subdirectory(OptimalPowerFlow)
endif()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would move the guard into the OptimalPowerFlow directory.

Comment on lines +379 to +380
static constexpr RealT DERIVATIVE_TOL = 8 * std::numeric_limits<RealT>::epsilon();
static constexpr RealT BALANCE_TOL = 1.0e-12;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, I would round 8 to 10. And replace 1e-12 with a multiple of std::numeric_limits<RealT>::epsilon();

Comment on lines +38 to +41
bool isZero(double value)
{
return !(value < ZERO<double>) && !(value > ZERO<double>);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've been trying to avoid these. If something needs == 0/!= 0, please highlight and we can evaluate.

return true;
}
std::cerr << std::setprecision(std::numeric_limits<RealT>::max_digits10) << value << " differs from "
<< reference << " by " << std::abs(value - reference) / (ONE<RealT> + std::abs(reference)) / std::numeric_limits<RealT>::epsilon()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't re-implement Testing::isEqual.

const RealT* x = system.x().getData();

success *= isEqual(x[1], ZERO<RealT>, BALANCE_TOL);
success *= std::abs(x[3]) > BALANCE_TOL && std::abs(x[5]) > BALANCE_TOL;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is checking the absolute values really needed instead of isEqual?

is >> br.fbus >> br.tbus >> br.r >> br.x >> br.b >> br.rateA >> br.rateB
>> br.rateC >> br.ratio >> br.angle >> br.status >> br.angmin
>> br.angmax;
checkEndOfMatrixRow(is);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing checkEndOfMatrixRow partially addresses #587, but we still need to ensure the read was successful (e.g., row not too short).

@pelesh pelesh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks very good and has clean component model structure. Main issues that I would raise are:

  • Separating equality and inequality constraint evaluation methods.
  • Selection of coordinates polar vs. Cartesian.
  • Inheritance patterns should mirror PhasorDynamics module. If we want to change it, then the change should made be across the GridKit modules.

* Filled by `mapped_store` and `lower_store`. Assembly sums duplicate
* coordinates.
*/
struct CooEntries

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this struct? This needs to be documented.

Comment on lines +30 to +33
using ScalarT = scalar_type;
using IdxT = index_type;
using RealT = typename ComponentModelT::RealT;
using ModelDataT = BranchData<RealT, IdxT>;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should not be public. ScalarT, IdxT, etc. are local type aliases.

Comment on lines +99 to +104
g[0] = p1 * p1 + q1 * q1;
g[1] = p2 * p2 + q2 * q2;
g[2] = p1;
g[3] = q1;
g[4] = p2;
g[5] = q2;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to specify here what are equality and what are inequality constraints.

Comment on lines +28 to +33
* @tparam model_type - Derived component model
*/
template <typename model_type, typename scalar_type, typename index_type>
class ComponentModel : public Component<scalar_type, index_type>
{
public:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest removing Component class and using the same inheritance structure as in PhasorDynamics module.

Comment on lines +310 to +312
const auto& terminals = component.terminals();
for (IdxT t = 0; t < terminals.size(); ++t)
{

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that we decided to use term "ports" rather than "terminals". It would be good to use consistent nomenclature across all GridKit modules.

Comment on lines +39 to +40
Number* x_u,
Index m,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are deprecated Ipopt types. Use ipnumber and ipindex instead.

Comment on lines +24 to +26
template <typename scalar_type, typename index_type>
class SteadyState : public Ipopt::TNLP
{

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not SteadyState but rather Ipopt specific interface. Perhaps use SteadyState for the namespace and IpoptInterface for the class name.

Comment on lines +43 to +47
public:
using ScalarT = scalar_type;
using IdxT = index_type;
using RealT = typename ScalarTraits<ScalarT>::RealT;
using CooEntriesT = Enzyme::Sparse::CooEntries;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These type aliases should not be public.

Comment on lines +81 to +88
std::vector<std::unique_ptr<ComponentT>> components_;

/// Buses by number
std::map<IdxT, BusT*> buses_;

/// Map from component entries to CSR entries
std::vector<IdxT> jacobian_map_;
std::vector<IdxT> hessian_map_;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would avoid using STL containers for system level objects. If it is a temporary solution, it should be marked as such.

Comment thread GridKit/Model/OptimizationEvaluator.hpp Outdated
Comment on lines +43 to +44
virtual int evaluateConstraints() = 0;
virtual int evaluateJacobian() = 0;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to separate equality and inequality constraints, as well as their respective Jacobians.

@lukelowry

Copy link
Copy Markdown
Collaborator Author

Thank you all for detailed review, I believe I have addressed all except a distinct few. I will go response by response soon, but the overall design is overall more consistent with PhasorDynamics except for the inheritance structure. I am considering removing the larger ACTIVSg2000 case until after this is merged, please let me know what you think.

@lukelowry
lukelowry requested a review from pelesh October 18, 2026 13:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants